package wrapper

import (
	"fmt"
	"github.com/natefinch/lumberjack"
	"github.com/rs/zerolog"
	"gopkg.in/yaml.v3"
	"os"
	"strconv"
	"strings"
	"sync"
	"unicode"
)

type Config struct {
	Path     string `yaml:"path"`
	FileName string `yaml:"file-name"`
	Level    string `yaml:"level"`
	Rotate   struct {
		Size           string `yaml:"size"`
		BackupLimit    int    `yaml:"backup-limit"`
		BackupExpire   int    `yaml:"backup-expire"` // unit: day
		BackupCompress bool   `yaml:"backup-compress"`
	} `yaml:"rotate"`
	Stdout bool `yaml:"stdout"`
}

func (c Config) fullFileName() string {
	path := c.Path
	if strings.HasSuffix(path, string(os.PathSeparator)) || strings.HasSuffix(path, "/") {
		return path + c.FileName
	}
	return path + string(os.PathSeparator) + c.FileName
}

func (c Config) rotateSize() int {
	s := strings.ToLower(c.Rotate.Size)
	if !strings.ContainsAny(s, "kmgt") {
		size := atoi(s)
		return size
	}

	size := 0
	end, start, i := len(s), 0, 0
	for i = 1; i < end; i++ {
		if unicode.IsDigit(rune(s[i])) {
			continue
		}

		shift, ok := byteUnitMap[s[i]]
		if !ok {
			panic("unknown byte unit: " + string(s[i]))
		}
		t := atoi(s[start:i])
		size += t << shift
		start = i + 1
	}
	if i >= end && start < end {
		size += atoi(s[start:])
	}
	return size
}

func (c Config) rotateSizeByMB() int {
	maxSize := c.rotateSize()
	ms := maxSize >> 20
	if 0 < (maxSize - (ms << 20)) {
		ms++
	}
	return ms
}

func atoi(s string) int {
	t, err := strconv.Atoi(s)
	if err != nil {
		panic(err)
	}
	return t
}

var byteUnitMap = map[byte]byte{
	'k': 10,
	'm': 20,
	'g': 30,
	't': 40,
}

const (
	defaultConfigName = "default"
)

func loadConfig(content []byte) (map[string]*Config, error) {
	configMap := map[string]*Config{}
	if err := yaml.Unmarshal(content, &configMap); err != nil {
		return nil, err
	}

	defaultConfig, ok := configMap[defaultConfigName]
	if !ok {
		return configMap, nil
	}

	for k, c := range configMap {
		if k == defaultConfigName {
			continue
		}

		if c.Path == "" {
			c.Path = defaultConfig.Path
		}
	}
	delete(configMap, defaultConfigName)
	return configMap, nil
}

func loadConfigFromFile(name string) (map[string]*Config, error) {
	content, err := os.ReadFile(name)
	if err != nil {
		return nil, err
	}

	return loadConfig(content)
}

func LoadLogsFromFile(name string) error {
	configs, err := loadConfigFromFile(name)
	if err != nil {
		return err
	}

	for name, c := range configs {
		logMap[name] = constructLog(c)
	}
	return err
}

func LoadLogs(content []byte) error {
	configs, err := loadConfig(content)
	if err != nil {
		return err
	}

	for name, c := range configs {
		logMap[name] = constructLog(c)
	}
	return err
}

func constructLog(c *Config) *zerolog.Logger {
	level, err := zerolog.ParseLevel(c.Level)
	if err != nil {
		panic(err)
	}
	inner := &lumberjack.Logger{
		MaxSize:    c.rotateSizeByMB(),
		MaxAge:     c.Rotate.BackupExpire,
		Compress:   c.Rotate.BackupCompress,
		MaxBackups: c.Rotate.BackupLimit,
		Filename:   c.fullFileName(),
		LocalTime:  true,
	}
	if !c.Stdout {
		logger := zerolog.New(inner).Level(level)
		return &logger
	}
	console := zerolog.ConsoleWriter{
		Out: os.Stdout,
	}
	logger := zerolog.New(zerolog.MultiLevelWriter(console, inner)).Level(level)
	return &logger
}

var logLock = sync.Mutex{}
var logMap = map[string]*zerolog.Logger{}

func Log(name string) *zerolog.Logger {
	logLock.Lock()
	defer logLock.Unlock()
	if target, ok := logMap[name]; ok {
		return target
	}
	panic(fmt.Sprintf("logger %s not exists", name))
}

func LazyLog(name string) func() *zerolog.Logger {
	return OnceValue(func() *zerolog.Logger {
		return Log(name)
	})
}

func OnceValue[T any](f func() T) func() T {
	var (
		once   sync.Once
		valid  bool
		p      any
		result T
	)
	g := func() {
		defer func() {
			p = recover()
			if !valid {
				panic(p)
			}
		}()
		result = f()
		valid = true
	}
	return func() T {
		once.Do(g)
		if !valid {
			panic(p)
		}
		return result
	}
}
